package com.ease.util {

import com.ease.util.ProfilingManager;
import com.ease.util.logging.LogManager;

import flash.events.Event;
import flash.events.EventDispatcher;
import flash.events.IEventDispatcher;
import flash.events.IOErrorEvent;
import flash.net.URLLoader;
import flash.net.URLRequest;
import flash.utils.Dictionary;

import mx.events.ResourceEvent;
import mx.logging.ILogger;
import mx.resources.IResourceManager;
import mx.resources.Locale;
import mx.resources.ResourceManager;
import mx.utils.StringUtil;

/**
 * This class provides localization support for applications whose resources are
 * either linked statically or are bundled as runtime modules (swfs). In order
 * to load resource modules at runtime the application should provide a resources
 * manifest (an XML file) instructing the <code>ResourceLoader</code> about
 * application locale chain and related resource module (swf) locations.
 *
 * <p>The resources manifest should be of the form:</p>
 * <listing version="3.0">
 *    <!ELEMENT resources ( locales?, resource+ ) >
 *    <!ELEMENT locales ( locale+ ) >
 *    <!ELEMENT locale ( #PCDATA ) >
 *    <!ATTLIST locale lang NMTOKEN #IMPLIED >
 *    <!ELEMENT resource ( module+ ) >
 *    <!ATTLIST resource locale NMTOKEN #REQUIRED >
 *    <!ATTLIST resource chain NMTOKEN #IMPLIED >
 *    <!ELEMENT module EMPTY >
 *    <!ATTLIST module uri NMTOKEN #REQUIRED >
 * </listing>
 *
 * <p>A sample manifest file:</p>
 * <listing version="3.0">
 *    <!--
 *       List of locales supported by the application. Unsupported locales will default to
 *       a locale ID (LCID) based on the language default. For example, fr_CA will default
 *       to fr_FR since the language "fr", as per the attribute (lang), defaults to fr_FR.
 *       For locale sensitive regions use LCID as the language default. For example,
 *       zh_CN should not be the default for zh_TW - an unsupported locale - zh_TW should
 *       rather default to en_US. Thus rather than using the lang="zh" use the LCID
 *       as the language default (lang="zh_CN") to emphasize that zh_CN is not the default
 *       language of choice for all chinese languages.
 *      -->
 *    <locales>
 *       <locale lang="en">en_US</locale>
 *       <locale lang="fr">fr_FR</locale>
 *       <locale lang="zh_CN">zh_CN</locale>
 *    <locales>
 *    <resources>
 *       <resource locale="ja_JP" chain="fr_FR">
 *          <module uri="locale/Resources_ja_JP.swf"/>
 *       </resource>
 *       <resource locale="fr_FR">
 *          <module uri="locale/Resources_fr_FR.swf"/>
 *       </resource>
 *    </resources>
 * </listing>
 * <br/>
 * Note1: The the last link in the chain always defaults to en_US. Also, default
 * to en_US if locale chain is not specified, or if the chain is empty.
 * <br/>
 * Note2: The special token "{locale}" could be used in the <code>locale</code>
 * and <code>uri</code> attributes to indicate a fallback <code>resource</code> entry.
 * This allows specifying a single resource entry that matches all the localization
 * locales. For example:<br/>
 * <listing version="3.0">
 *    <resources>
 *       <resource locale="{locale}">
 *          <module uri="locale/Resources_{locale}.swf"/>
 *       </resource>
 *    </resources>
 * </listing>
 *
 * <p>Usage Sample 1:</p>
 * <listing version="3.0">
 *    var resourceInfo:ResourceInfo = new ResourceInfo();
 *    resourceInfo.resourceManifestUri = "/ngclient/config/resources.xml";
 *    // Set the application locale before loading the resources. If not
 *    // set the ResourceLoader will default to en_US.
 *    ResourceLoader.instance().localeChain = ["ja_JP"];
 *    ResourceLoader.instance().loadResources(resourceInfo, "", callback);
 *    // ...
 *    ResourceLoader.instance().getString("myResource", "productName");
 *    // ...
 *    private function callback(callContext:Object, errors:Array):void {
 *       // ...
 *    }
 * </listing>
 * </p>
 * <p>Usage Sample 2:</p>
 * <listing version="3.0">
 *    var resourceInfo:ResourceInfo = new ResourceInfo();
 *    resourceInfo.resourceManifestXml = <resources>...</resources>;
 *    // Set the application locale before loading the resources. If not
 *    // set the ResourceLoader will default to en_US.
 *    ResourceLoader.instance().localeChain = ["ja_JP"];
 *    ResourceLoader.instance().loadResources(resourceInfo, "", callback);
 *    // ...
 *    ResourceLoader.instance().getString("myResource", "productName");
 * </listing>
 * </p>
 * <p>Usage Sample 3:</p>
 * <listing version="3.0">
 *    var resourceInfo:ResourceInfo = new ResourceInfo();
 *    resourceInfo.resourceModuleURIs = [ "locale/Resources_ja_JP.swf" ];
 *    // Set the application locale before loading the resources. If not
 *    // set the ResourceLoader will default to en_US.
 *    ResourceLoader.instance().localeChain = ["ja_JP"];
 *    ResourceLoader.instance().loadResources(resourceInfo, "", callback);
 *    // ...
 *    ResourceLoader.instance().getString("myResource", "productName");
 * </listing>
 * </p>
 *
 * @see ResourceInfo
 */
public class ResourceLoader extends EventDispatcher {
   private static const _logger:ILogger = LogManager.getLogger(ResourceLoader);

   private static const DEFAULT_LOCALE:String = "en_US";
   // The pattern string specified in the uri and locale attributes to be replaced
   // by the locale string.
   private static const LOCALE_TOKEN:String = "{locale}";
   private static var _instance:ResourceLoader = null;

   private var _resourceManager:IResourceManager = null;
   private var _localeChain:Array = null;
   private var _applicationLocale:String;
   /**
    * Resource base url to ModulesLoadInfo object map.
    * This is used to monitor the loading process as well
    * as a cache to indicate which modules have been loaded.
    */
   private var _baseUrlToLoadInfo:Dictionary = new Dictionary();

   /**
    * @return The one and only instance of ResourceManager.
    */
   public static function instance():ResourceLoader {
      if (_instance == null) {
         _instance = new ResourceLoader(PrivateConstructorToken);
      }

      return _instance;
   }

   /**
    * @private Constructor. As a singleton, this class constructor prevents
    * instantiation from other classes. Use ResourceLoader.instance()
    * to work with the singleton from other classes.
    *
    * @throws Error if the constructor is called by an external class or if
    *    the singelton has already been instantiated.
    */
   public function ResourceLoader(token:Class) {
      if (token != PrivateConstructorToken) {
         // Should this be localized? This error should be caught
         // during development. If not our testing has failed badly.
         throw new Error("ResourceLoader cannot be instantiated directly.");
      }

      _resourceManager = ResourceManager.getInstance();
      _applicationLocale = DEFAULT_LOCALE;
   }

   /**
    * Gets the value of a specified resource key as String. If the
    * resource is not found then returns the passed in resource key
    * as value.
    *
    * <p>If the <code>parameters</code> argument is passed to this method,
    * the parameters in it are converted to Strings
    * and then substituted, in order, for the placeholders
    * <code>"{0}"</code>, <code>"{1}"</code>, and so on, in the String
    * before it is returned.</p>
    *
    * @param bundleName The name of a resource bundle.
    * @param resourceKey The name of a resource in the resource bundle.
    */
   public function getString(bundleName:String,
                             resourceKey:String,
                             parameters:Array = null):String {
      var value:String =
            _resourceManager.getString(bundleName, resourceKey, parameters);
      if (StringUtils.isNullorEmpty(value)) {
         value = resourceKey;
      }

      return value;
   }

   /**
    * Gets the value of a specified resource key as Class. Returns
    * null if:
    * <ul>
    * <li>the resource value was not a class reference.</li>
    * <li>the specified resource key is not found.</li>
    *
    * @param bundleName The name of a resource bundle.
    * @param resourceKey The name of a resource in the resource bundle.
    */
   public function getClass(bundleName:String,
                            messageKey:String):Class {
      return _resourceManager.getClass(bundleName, messageKey);
   }

   /**
    * Gets the value of a specified resource key as Object. Returns
    * null if:
    * <ul>
    * <li>the resource value was not a class reference.</li>
    * <li>the specified resource key is not found.</li>
    *
    * @param bundleName The name of a resource bundle.
    * @param resourceKey The name of a resource in the resource bundle.
    */
   public function getObject(bundleName:String,
                            messageKey:String):Object {
      return _resourceManager.getObject(bundleName, messageKey);
   }

   /**
    * An Array of locale Strings, such as <code>[ "en_US" ]</code>,
    * which specifies one or more locales to be searched for resources.
    * <p>
    * The <code>ResourceLoader</code> starts with the first locale in the
    * <code>localeChain</code> and looks for a resource bundle
    * with the specified bundle name for that locale. If the
    * specified resource does not exist in the bundle then the
    * loader proceeds on to the other locales in the <code>localeChain</code>.
    * </p>
    */
   public function set localeChain(value:Array):void {
      if (value == null || value.length == 0) {
         return;
      }

      _localeChain = value;
      _resourceManager.localeChain = _localeChain;
   }

   /**
    * The primary locale for the current application.
    */
   public function set applicationLocale(value:String):void {
      _applicationLocale = value;
   }

   public function get applicationLocale():String {
      return _applicationLocale;
   }

   /**
    * Begins loading resource modules whose locations are supplied via the
    * <code>ResourceInfo</code> class.
    *
    * @param resourceInfo Contains information necessary to load
    * resouce bundles packaged as resource modules.
    *
    * @param callContext The context object handed back to the caller
    * after the resource loading has completed.
    *
    * @param callback Load completion callback:
    * <code>function(callContext:Object, errors:Array)</code>
    *
    */
   public function loadResources(resourceInfo:ResourceInfo,
                                 callback:Function,
                                 callContext:Object = null):void {
      if (! StringUtils.isNullorEmpty(resourceInfo.resourceManifestUri)) {
         loadResourceManifest(resourceInfo, callback, callContext);
      } else if (resourceInfo.resourceManifestXml != null) {
         resourceInfo = processManifestXml(resourceInfo);
         loadResourceModules(resourceInfo, callback, callContext);
      } else if (resourceInfo.resourceModuleURIs != null) {
         loadResourceModules(resourceInfo, callback, callContext);
      }
   }

   /**
    * Checks whether the resource modules associated with the given base url are
    * loaded.
    *
    * @param resourceBaseUrl
    *    The base URL for all resource module URIs.
    */
   public function areModulesLoaded(resourceBaseUrl:String):Boolean {
      var loadInfo:ModulesLoadInfo = _baseUrlToLoadInfo[resourceBaseUrl];
      if (loadInfo == null) {
         return false;
      }
      return !loadInfo.inProgress;
   }

   private function loadResourceManifest(resourceInfo:ResourceInfo,
                                         callback:Function,
                                         callContext:Object = null):void {
      var loader:ManifestLoader = new ManifestLoader(
            resourceInfo, callback, callContext);
      loader.addEventListener(Event.COMPLETE, onManifestLoadSuccess);
      loader.addEventListener(IOErrorEvent.IO_ERROR, onManifestLoadFailure);

      var manifestUrl:String = resolveUrl(
               resourceInfo.resourceBaseUrl, resourceInfo.resourceManifestUri);
      loader.load(new URLRequest(manifestUrl));
   }

   private function loadResourceModules(resourceInfo:ResourceInfo,
                                        callback:Function,
                                        callContext:Object = null):void {
      Diagnostics.assert(callback != null
         , "callback is a required parameter to loadResourceModules");

      if (resourceInfo.resourceModuleURIs == null ||
          resourceInfo.resourceModuleURIs.length == 0) {
          // No resource modules to load. The very least notify the caller
          // that the operation is complete.
          callback(callContext, null);
          return;
      }

      var loadInfo:ModulesLoadInfo =
         _baseUrlToLoadInfo[resourceInfo.resourceBaseUrl];
      if (loadInfo != null) {
         if (loadInfo.inProgress) {
            loadInfo.callbacks.push(new CallbackInfo(callback, callContext));
         } else {
            callback(callContext, loadInfo.errors);
         }
         return;
      } else {
         loadInfo = new ModulesLoadInfo();
         loadInfo.inProgress = true;
         loadInfo.callbacks.push(new CallbackInfo(callback, callContext));
         _baseUrlToLoadInfo[resourceInfo.resourceBaseUrl] = loadInfo;
      }
      // Set locale chain only if it is not already set by the application.
      if (_localeChain == null) {
         extractAndSaveLocaleChain(resourceInfo);
      }
      if (ProfilingManager.instance.enabled) {
         loadInfo.profilingToken = ProfilingManager.instance.logStartOperation(_logger,
               "loadResourceModules-start: {0}", resourceInfo.resourceBaseUrl);
      }
      for each(var moduleUri:String in resourceInfo.resourceModuleURIs) {
         var moduleUrl:String = resolveUrl(
               resourceInfo.resourceBaseUrl, moduleUri);
         var eventDispatcher:IEventDispatcher =
               _resourceManager.loadResourceModule(moduleUrl);
         eventDispatcher.addEventListener(ResourceEvent.COMPLETE, onModuleLoad);
         eventDispatcher.addEventListener(ResourceEvent.ERROR, onModuleLoad);
         loadInfo.dispatchers.push(eventDispatcher);
      }
   }

   /**
    * The ResourceEvent handler for both success and failure.
    */
   private function onModuleLoad(event:ResourceEvent):void {
      var dispatcher:IEventDispatcher = IEventDispatcher(event.target);
      dispatcher.removeEventListener(ResourceEvent.COMPLETE, onModuleLoad);
      dispatcher.removeEventListener(ResourceEvent.ERROR, onModuleLoad);

      // look for the dispatcher in _baseUrlToLoadInfo
      var loadInfo:ModulesLoadInfo;
      for each (var mli:ModulesLoadInfo in _baseUrlToLoadInfo) {
         // mli.dispatchers is set to null when the module is loaded and
         // its dispatchers are notified.
         if (!mli.inProgress) {
            continue;
         }
         var index:int = mli.dispatchers.indexOf(dispatcher);
         if (index >= 0) {
            mli.dispatchers.splice(index, 1);
            loadInfo = mli;
            break;
         }
      }
      Diagnostics.assert(loadInfo != null
         , "Resource module loading event dispatcher not found!");

      if (!StringUtils.isNullorEmpty(event.errorText)) {
         loadInfo.errors.push(new Error(event.errorText));
      }
      // if this is the last module to load then perform the callbacks
      // and null out for gc.
      if (loadInfo.dispatchers.length == 0) {
         if (loadInfo.profilingToken != null) {
            ProfilingManager.instance.logEndOperation(_logger, loadInfo.profilingToken,
                  "loadResourceModules-end");
         }
         for each (var cbi:CallbackInfo in loadInfo.callbacks) {
            cbi.callback(cbi.callContext, loadInfo.errors);
         }
         loadInfo.inProgress = false;
         loadInfo.dispatchers = null;
         loadInfo.callbacks = null;
         if (loadInfo.errors.length == 0) {
            loadInfo.errors = null;
         }
      }
   }

   /**
    * Extracts resource module URIs, defined in the manifest XML, based on
    * the application's locale and the locale chain, and populates the
    * <code>ResourceInfo</code> object.
    */
   private function processManifestXml(resourceInfo:ResourceInfo):ResourceInfo {
      var localChain:Array = getLocaleChain(resourceInfo);

      var resources:XML = resourceInfo.resourceManifestXml;
      resourceInfo.resourceModuleURIs = [];

      var fallbackResource:XMLList = resources.resource.
            (hasOwnProperty("@locale") && @locale == LOCALE_TOKEN);

      for each (var locale:String in localChain) {
         var resource:XMLList = resources.resource.
               (hasOwnProperty("@locale") && @locale == locale);
         if (resource.length() == 0) {
            resource = fallbackResource;
         }
         for each (var module:XML in resource.module) {
            var uri:String = module.@uri;
            if (StringUtils.isNullorEmpty(uri)) {
               continue;
            }
            uri = uri.replace(LOCALE_TOKEN, locale);
            resourceInfo.resourceModuleURIs.push(uri);
         }
      }

      return resourceInfo;
   }

   /**
    * Returns the application's locale chain. If the application locale chain is not set
    * then extracts the locale chain information, for the application locale, from the
    * resource manifest.
    */
   private function getLocaleChain(resourceInfo:ResourceInfo):Array {
      if (_localeChain != null) {
         return _localeChain;
      }
      var retval:Array = extractAndSaveLocaleChain(resourceInfo);
      return retval;
   }

   /**
    * Extracts the locale chain information, for the application locale,
    * from the resource manifest. If the resource manifest is not provided
    * then returns the application's locale and the default locale as the
    * locale chain.
    */
   private function extractAndSaveLocaleChain(resourceInfo:ResourceInfo):Array {
      var applicationLocale:String = getVerifiedAppLocale(resourceInfo);
      var appLocaleChain:Array = [ applicationLocale ];
      if (resourceInfo.resourceManifestXml != null) {
         var resources:XML = resourceInfo.resourceManifestXml;
         // Get the resource information that matches the current
         // application's locale.
         var resource:XMLList = resources.resource.
            (hasOwnProperty("@locale") && @locale == _applicationLocale);
         var localeChainString:String =
            StringUtil.trimArrayElements(resource.@chain, ",");
         var localeChainArray:Array = localeChainString.split(",");
         appLocaleChain = appLocaleChain.concat(localeChainArray);
      }
      appLocaleChain.push(DEFAULT_LOCALE);
      // Remove duplicate and empty locale strings.
      appLocaleChain = appLocaleChain.filter(
         function (element:String, index:int, array:Array):Boolean {
            return (array.indexOf(element) == index) &&
            (! StringUtils.isNullorEmpty(element));
         }
      );
      this.localeChain = appLocaleChain;
      return appLocaleChain;
   }

   /**
    * Verifies if the current application locale is one of the locales supported by
    * the application. If the locale is not supported then resets the application locale
    * to the DEFAULT_LOCALE. If the locale did not include country code or if the locale
    * differs only by the country code then sets the first supported locale, that starts
    * with the current locale language, as the application locale.
    */
   private function getVerifiedAppLocale(resourceInfo:ResourceInfo):String {
      if (resourceInfo.resourceManifestXml == null) {
         return _applicationLocale;
      }
      var resources:XML = resourceInfo.resourceManifestXml;
      if (resources.locales == null) {
         return _applicationLocale;
      }
      var retval:String;
      var appLocaleObj:Locale = new Locale(_applicationLocale);
      for each (var locale:XML in resources.locales.locale) {
         var supportedLocale:String = StringUtil.trim(locale);
         if (StringUtils.isNullorEmpty(supportedLocale)) {
            continue;
         }
         if (supportedLocale == _applicationLocale) {
            retval = _applicationLocale;
            break;
         }
         // Check if the application locale's language matches with the supported locale's
         // language default. If it does then the partially matched supported locale has
         // the potential to become the application locale provided the application locale
         // does not completely match with any of the supported locales.
         if (StringUtils.isNullorEmpty(retval)) {
            var supportedLanguage:String;
            if (locale.hasOwnProperty("@lang")) {
               supportedLanguage = StringUtil.trim(locale.@lang);
            } else {
               var supportedLocaleObj:Locale = new Locale(supportedLocale);
               supportedLanguage = supportedLocaleObj.language;
            }
            if (supportedLanguage == appLocaleObj.language) {
               retval = supportedLocale;
            }
         }
      }
      if (StringUtils.isNullorEmpty(retval)) {
         retval = DEFAULT_LOCALE;
      }
      _applicationLocale = retval;
      _logger.info("Application locale is set to:", _applicationLocale);
      return retval;
   }

   private function resolveUrl(contextPath:String, relativeUri:String):String {
      if (! StringUtils.isNullorEmpty(contextPath)) {
         // Add trailing forward slash, if its not a part of the context path,
         // to ensure that the resolved URL is valid after appending the
         // relative URI to the context path.
         var index:int = contextPath.indexOf("/", contextPath.length - 1);
         if (index < 0) {
            contextPath += "/";
         }
      } else {
         contextPath = "";
      }

      var retval:String = contextPath;
      if (! StringUtils.isNullorEmpty(relativeUri)) {
         retval += relativeUri;
      }

      return retval;
   }

   private function onManifestLoadSuccess(event:Event):void {
      var loader:ManifestLoader = ManifestLoader(event.target);
      loader.removeEventListener(Event.COMPLETE, onManifestLoadSuccess);
      loader.removeEventListener(IOErrorEvent.IO_ERROR, onManifestLoadFailure);

      loader.resourceInfo.resourceManifestXml = new XML(loader.data);
      loader.resourceInfo = processManifestXml(loader.resourceInfo);
      loadResourceModules(loader.resourceInfo, loader.callback, loader.callContext);
   }

   private function onManifestLoadFailure(event:IOErrorEvent):void {
      var loader:ManifestLoader = ManifestLoader(event.target);
      loader.removeEventListener(Event.COMPLETE, onManifestLoadSuccess);
      loader.removeEventListener(IOErrorEvent.IO_ERROR, onManifestLoadFailure);

      if (loader.callback != null) {
         loader.callback(loader.callContext, [ new Error(event.text) ]);
      }
   }
}

}

////////////////////////////////////////////////////////////////////////
// Private class
////////////////////////////////////////////////////////////////////////

import com.ease.util.ResourceInfo;

import flash.net.URLLoader;

/**
 * @private
 */
class ManifestLoader extends URLLoader {
   public var resourceInfo:ResourceInfo;
   public var callContext:Object;
   public var callback:Function;

   public function ManifestLoader(resourceInfo:ResourceInfo,
                                  callback:Function,
                                  callContext:Object) {
      this.resourceInfo = resourceInfo;
      this.callback = callback;
      this.callContext = callContext;
   }
}

/**
 * Contains information about the resource modules loading
 * context.
 */
class ModulesLoadInfo {
   /**
    * Modules loading in progress.
    */
   public var inProgress:Boolean;

   [ArrayElementType("CallbackInfo")]
   /**
    * The callbacks to perform when the loading is done.
    * Null if modules finished loading.
    */
   public var callbacks:Array = [];

   [ArrayElementType("flash.events.IEventDispatcher")]
   /**
    * The resource modules event dispatchers returned from loadResourceModule.
    * Null if modules finished loading.
    */
   public var dispatchers:Array = [];

   [ArrayElementType("Error")]
   /**
    * The resource modules loading errors.
    */
   public var errors:Array = [];

   public var profilingToken:String;
}

/**
 * Contains information about a callback.
 */
class CallbackInfo {
   public var callback:Function;
   public var callContext:Object;

   public function CallbackInfo(callback:Function, callContext:Object) {
      this.callback = callback;
      this.callContext = callContext;
   }
}

// ActionScript 3.0 allows you to include multiple classes in one source file,
// but only one class in each file can be made available to code that is
// external to that file. In other words, only one class in each file can be
// declared inside a package declaration. You must declare any additional
// classes outside your package definition, which makes those classes invisible
// to code outside that source file.
//
// Programming ActionScript 3.0 / ActionScript language and syntax / Packages
// and namespaces / Packages / Creating packages
//
// http://livedocs.adobe.com/flex/3/html/03_Language_and_Syntax_05.html#151506
class PrivateConstructorToken {}
